package com.lhl.mq.mqServer.datacenter;

import com.lhl.mq.common.BinaryTool;
import com.lhl.mq.common.MqException;
import com.lhl.mq.mqServer.core.MSGQueue;
import com.lhl.mq.mqServer.core.Message;
import org.apache.coyote.http11.filters.VoidInputFilter;

import java.io.*;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.SortedMap;

/**
 * 通过这个类来对硬盘上的消息进行管理
 */
public class MessageFileManager {
    //使用一个内部类来表示队列的统计消息
    //使用静态内部类
    static public class Stat{
        //直接使用public，这样就不需要写get set 方法了
        public int totalCount;//总消息数量
        public int validCount;//有效消息数量
    }

    //暂时不使用初始化
    public void init(){

    }

    //用于获取到指定队列对应的消息文件所在的路径
   private String getQueueDir(String queueName){
        return "./data/" + queueName;
    }

    //用于获取队列的消息数据的路径
   private String getQueueDataPath(String queueName){
        return getQueueDir(queueName) + "/queue_data.txt";
    }

    //用于获取队列的统计数据的文件的路径
   private String getQueueStatPath(String queueName){
        return getQueueDir(queueName) + "/queue_stat.txt";
    }

    //读取统计文件内容
    private Stat readStat(String queueName){
        //当前统计消息文件是文本文件，可以直接使用scanner来读取文件
        Stat stat = new Stat();
        try (InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))) {
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        }catch (IOException e){
            e.printStackTrace();
        }
      return null;
    }

    //将数据写入统计文件中
    private void writeStat(String queueName,Stat stat){
        //使用 PrintWrite 经行文件的写入
        //注意点：OutputStream 打开文件，默认的情况下，会直接把原文件清空，此时相当于新数据覆盖了旧数据
        try (OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))) {
          PrintWriter printWriter = new PrintWriter(outputStream);
          printWriter.write(stat.totalCount + "\t" + stat.validCount);
          printWriter.flush();
        }catch (IOException e){
            e.printStackTrace();
        }

    }

    //创建队列对应的文件和目录
    public void createQueueFiles(String queueName) throws IOException {
        //1、先创建队列对应的消息目录
        File baseDir = new File(getQueueDir(queueName));
        if (!baseDir.exists()){
            //不存在这个目录则创建
            boolean ok = baseDir.mkdirs();
            if (!ok){
                throw new IOException("创建目录失败！ baseDir = " + baseDir.getAbsoluteFile());
            }
        }

        //2、创建文件数据文件
        File dataFile = new File(getQueueDataPath(queueName));
        if (!dataFile.exists()){
            //不存在则创建
            boolean ok = dataFile.createNewFile();
            if (!ok){
                throw new IOException("创建数据文件失败！dataFile = " + dataFile.getAbsoluteFile());
            }
        }
        //3、创建统计数据文件
        File statFile = new File(getQueueStatPath(queueName));
        if (!statFile.exists()){
            //不存在则创建
            boolean ok = statFile.createNewFile();
            if (!ok){
                throw new IOException("创建统计数据文件失败！ statFile = " + statFile.getAbsoluteFile());
            }
        }
        //4、给统计文件设置初始值
        Stat stat = new Stat();
        stat.validCount = 0;
        stat.totalCount = 0;
        writeStat(queueName,stat);
    }

    //3、删除目录和文件
    public void deleteQueueFiles(String queueName) throws IOException {
        //先删除文件(删除目录前要保证这个目录是空目录)
        File dataFile = new File(getQueueDataPath(queueName));
        boolean ok1 = dataFile.delete();
        File statFile = new File(getQueueStatPath(queueName));
        boolean ok2 = statFile.delete();

        //删除目录
        File dirFile = new File(getQueueDir(queueName));
        boolean ok3 = dirFile.delete();

        if (!ok1 || !ok2 || !ok3){
            throw new IOException("队列和目录删除失败");
        }
    }

    //4、检查目录和文件是否存在
    public boolean checkFilesExits(String queueName){
        File dataFile = new File(getQueueDataPath(queueName));
        boolean ok1 = dataFile.exists();
        File statFile = new File(getQueueStatPath(queueName));
        boolean ok2 = statFile.exists();

        if (!ok1 || !ok2){
            return false;
        }

        return true;
    }

    //5、把一个消息以追加的方式放到文件中
    public void sendMessage(MSGQueue queue, Message message) throws MqException, IOException {
        //查看队列对应的目录中的文件是否存在
        if (!checkFilesExits(queue.getName())) {
            //不存则抛出异常
            throw new MqException("[MessageFileManager] 新增消息失败，队列对应的文件不存在，queueName：" + queue.getName());
        }

        //将 message 进行序列化，转为一个二进制的字节数组
        byte[] messageBinary = BinaryTool.toBytes(message);
        synchronized (queue) {
            //设置offsetBeg、offsetEng
            File messsageFile = new File(getQueueDataPath(queue.getName()));
            message.setOffsetBeg(messsageFile.length() + 4);
            message.setOffsetEng(messsageFile.length() + 4 + messageBinary.length);
            //将消息长度和消息写入到文件中
            try (OutputStream outputStream = new FileOutputStream(messsageFile, true)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    //将消息长度以四个字节的形式写入到文件中
                    dataOutputStream.writeInt(messageBinary.length);
                    //将序列化后的消息写入到文件中
                    dataOutputStream.write(messageBinary);
                }
            }
            //更新统计文件的内容
            Stat stat = readStat(queue.getName());
            stat.totalCount += 1;
            stat.validCount += 1;
            writeStat(queue.getName(), stat);
        }
    }

    //删除消息
    public void deleteMessage(MSGQueue queue,Message message) throws IOException, ClassNotFoundException {
        File messageFile = new File(getQueueDataPath(queue.getName()));
        synchronized (queue){
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(messageFile, "rw")) {
            //把消息从文件中取出来
            byte[] bufferSrc = new byte[(int) ( message.getOffsetEng() - message.getOffsetBeg())];
            randomAccessFile.seek(message.getOffsetBeg());
            randomAccessFile.read(bufferSrc);
            //将数据转换为对象
             Message diskMessage=(Message) BinaryTool.fromBytes(bufferSrc);
            //修改消息对象的isValid
             diskMessage.setIsValid((byte) 0x0);

            //将修改后的对象转换为二进制数据
            byte[] lastMessage = BinaryTool.toBytes(diskMessage);
            //将光标移动回开始的位置
            randomAccessFile.seek(message.getOffsetBeg());
            //写回文件中
            randomAccessFile.write(lastMessage);
        }
        //删除消息后，更新统计文件的数据
        Stat stat = readStat(queue.getName());
            if (stat.validCount > 0) {
                stat.validCount -= 1;
            }
            writeStat(queue.getName(),stat);
    }
    }

    //加载队列中所有的消息到一个链表里
    //由于该方法是在服务启动时就进行调用的，此时还不涉及到多线程，所有不需要加锁
    //服务重启的时候内存里面的数据会全部清除掉，此时需要从硬盘中存储的数据到内存中
    public LinkedList<Message> loadMessageAllFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
        LinkedList<Message> messages = new LinkedList<>();
        //将消息从队列中拿出来
        try (InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))) {
            try (DataInputStream dataInputStream = new DataInputStream(inputStream)) {
                //该变量记录当前光标的位置
                int currentOffSet = 0;
                //使用循环将文件中的消息全部读完
                while (true){
                    //获取消息长度（size）里面记录的数据
                    int messageSize = dataInputStream.readInt();
                    //定义一个长度为（size）的byte数组
                    byte[] messageBytes = new byte[messageSize];
                    //从文件中读取消息内容
                    int actualSize = dataInputStream.read(messageBytes);
                    //将size与获取到的消息长度进行比较
                    if (actualSize != messageSize){
                        //记录的消息长度和消息的实际长度不一样，则说明消息的格式出现错乱
                        throw new MqException("[messageFileManager] 记录的消息长度和消息的实际长度不一致");
                    }

                    //将获取到的二进制数据转换为消息对象
                   Message message = (Message)BinaryTool.fromBytes(messageBytes);
                    //查看该消息是否有效，如果无效则直接跳过该消息
                    if (message.getIsValid() == 0x0){
                        //更新光标的位置
                        currentOffSet += (messageSize + 4);
                        //结束此次循环
                        continue;
                    }
                    //设置message的offsetBeg和offsetEng
                    message.setOffsetBeg(currentOffSet + 4);
                    message.setOffsetEng(currentOffSet + 4 + messageSize);
                    //更新当前光标的位置
                    currentOffSet += (4 + messageSize);
                    //将读取到的对象放入链表中
                    messages.add(message);
                }

            }catch (EOFException e){
                //这个异常是表示文件读取完成
                System.out.println("[MessageFileManager] 数据读取完成");
            }

        }
        //返回结果
        return messages;
    }

    //判断该队列是否需要进行GC
    public boolean checkGC(String queueName){
        Stat stat = readStat(queueName);
        if (stat.totalCount > 2000 && (double) stat.validCount /stat.totalCount < 0.5){
            return true;
        }
        return false;
    }

    //获取存放数据的临时文件的路径
    public String getNewDataFilePath(String queueName){
        return getQueueDir(queueName) + "/queue_data_new.txt";
    }


    //进行GC
    public void gc(MSGQueue queue) throws MqException, IOException, ClassNotFoundException {

        synchronized (queue) {
            //获取gc开始的时间
            long fastMillis = System.currentTimeMillis();

            String queueName = queue.getName();
            File newDataFile = new File(getNewDataFilePath(queueName));
            //判断该文件是否存在
            if (newDataFile.exists()) {
                //如果该文件存在的话，则说明上一次GC的时候，文件没有删除干净，则抛出异常
                throw new MqException("[MessageFileManager] gc时发现临时文件已存在，文件创建失败，queueName = " + queueName);
            }
            //创建文件
            boolean ok = newDataFile.createNewFile();
            if (!ok){
                //创建文件失败
                throw new MqException("[MessageFileManger] 创建文件失败，newDataFile = " + newDataFile.getAbsoluteFile());
            }

            //将旧文件中所有有效的数据都复制到新的文件中
            LinkedList<Message> messages = loadMessageAllFromQueue(queueName);
            //把有消息写入到新文件中
            try (OutputStream outputStream = new FileOutputStream(newDataFile)) {
                //怎么读的就怎么写
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    for (Message message : messages) {
                        if (message.getIsValid() == 0x1) {
                            byte[] bytes = BinaryTool.toBytes(message);
                            //先读四个字节
                            dataOutputStream.writeInt(bytes.length);
                            dataOutputStream.write(bytes);
                        }
                    }
                }
            }

            //将旧文件删除
            File oldDataFile = new File(getQueueDataPath(queueName));
            ok = oldDataFile.delete();
            //如果删除旧文件失败则抛出异常
            if (!ok) {
                throw new MqException("[MessageFileManager] 删除旧文件失败，queueName = " + queueName);
            }

            //将新文件重命名为旧文件的名字
            ok = newDataFile.renameTo(oldDataFile);
            //如果重命名失败
            if (!ok) {
                throw new MqException("[MessageFileManager] 新文件重命名失败， queueName = " + queueName);
            }

            //更新stat里面的数据
            Stat stat = readStat(queueName);
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(queueName, stat);
            //获取gc后的时间
            long lastMillis = System.currentTimeMillis();

            System.out.println("[MessageFileManager] 消息文件gc成功，用时 = " + (lastMillis - fastMillis) + ",queueName = " + queueName);
        }
    }
}
